package com.google.ant.extensions.tasks;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import com.google.ant.extensions.types.Dependencies;
import com.google.ant.extensions.types.Dependency;
import com.google.ant.extensions.types.DependencyHelper;
import com.google.ant.extensions.types.Mirror;
import com.google.ant.extensions.types.Repository;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Get;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.util.FileUtils;

public class FetchTask extends AbstractFantTask {
    private static final String PRIVATE_REPOSITORY_PROP_NAME = "private.repository.dir";
    private static final String REMOTE_REPOSITORY_PROP_NAME = "remote.repository";

    private static final String PROJECT_LIB_DIR = "lib";

    Get getTask;

    File privateRepository;
    File projectLib;

    boolean keepJars;
    boolean chained = true;

    private List<Dependency> dependencies = new ArrayList<Dependency>();

    private Map<String, List<String>> sites = new HashMap<String, List<String>>();

    public void setKeepJars(boolean b) {
        this.keepJars = b;
    }

    public List<Dependency> getDependencies() {
        return dependencies;
    }

    private List<File> removeOtherVersions(final Dependency dependency) {
        List<File> olderVersions = com.google.util.FileUtils.getFiles(projectLib,
                                                                      dependency.getArtifactid() + "-\\w+\\.?.+\\.jar$",
                                                                      loadFromProjectLib(dependency));

        if (verbose && !olderVersions.isEmpty()) {
            System.out.println("\n Found other version of " + dependency.getArtifactid()
                               + ":\n" + olderVersions);
        }
        return olderVersions;
    }

    private void removeJars(final Collection<File> jars, final String msg) {
        for (File jar : jars) {
            System.out.println(msg + jar);
            FileUtils.delete(jar);
        }
    }

    public void execute() {
        if (dependencies == null) {
            throw new BuildException("No dependencies defined");
        }

        resolveProjectLib();

        printInfo();

        Collection<File> toRemove = new ArrayList<File>();

        Collection<Dependency> allDependencies = this.chained
            ? new DependencyHelper(getProject(), this.dependencies).getAllDependencies() : this.dependencies;

        for (Dependency dependency : allDependencies) {
            File prjArtifact = loadFromProjectLib(dependency);
            if (toRemove.contains(prjArtifact)) {
                toRemove.remove(prjArtifact);
            }

            if (verbose) {
                System.out.println("\n----------------------");
                System.out.println("Resolving artifact " + dependency);
                System.out.println("----------------------");
            }

            toRemove.addAll(removeOtherVersions(dependency));
            File localArtifact = loadFromLocalBase(dependency);
            File privateArtifact = loadFromPrivateBase(dependency);
            if (isInProject(dependency)) {
                if (verbose) {
                    System.out.println("\n Found " + dependency + " in project lib ");
                }
                if (isUpToDate(prjArtifact, localArtifact)
                    && isUpToDate(prjArtifact, privateArtifact)) {
                    continue;
                } else {
                    System.out.println("  \n File " + dependency + " in local/private respository is newer than the one in the project");
                }
            } else {
                System.out.println("\n Not found " + dependency + " in project lib ");
            }

            File artifact = localArtifact;
            if (artifact != null) {
                if (verbose) {
                    System.out.println("\n Found " + dependency + " in local repository ");
                }
                if (isUpToDate(artifact, privateArtifact)) {
                    copy(artifact, projectLib);
                    continue;
                }
                System.out.println("\n " + dependency + " updated in private repository ");
            } else {
                System.out.println("\n Not found " + dependency + " in local repository ");
            }

            if (privateRepository != null) {
                artifact = privateArtifact;
                if (artifact != null) {
                    if (verbose) {
                        System.out.println("\n Found " + dependency + " in private repository ");
                    }

                    copy(artifact, localRepository);
                    copy(artifact, projectLib);
                    continue;
                }

                System.out.println("\n Not found " + dependency + " in private repository ");
            }
            downloadAndCopy(dependency);
        }
        cleanUpProjectLib(toRemove, allDependencies);
    }

    private void cleanUpProjectLib(final Collection<File> toRemove, 
                                   final Collection<Dependency> allDependencies) {
        if (keepJars) {
            return;
        }

        if (toRemove.isEmpty()) {
            return;
        }

        System.out.println("To Remove " + toRemove);

        Set<String> depJars = new HashSet<String>();
        for (Dependency dependency : allDependencies) {
            if (toRemove.contains(dependency)) {
                System.out.println("We do depend on " + dependency);
                toRemove.remove(loadFromProjectLib(dependency));
            }
            depJars.add(dependency.getArtifact());
        }

        List<File> allJars = com.google.util.FileUtils.getFiles(projectLib, ".+\\.jar$", null);
        List<File> redundant = new ArrayList<File>();
        for (File jar : allJars) {
            if (!depJars.contains(jar.getName())) {
                redundant.add(jar);
            }
        }
        removeJars(toRemove, "Remove other version jars ");
        removeJars(redundant, "Remove redundant jars ");
    }

    private void downloadAndCopy(Dependency dependency) {
        download(dependency);
        File artifact = getArtifactPath(localRepository, dependency);
        if (artifact.exists()) {
            copy(artifact, projectLib);
        } else {
            System.out.println("\n Downloaded artifact " + artifact + " can not be found, try again");
        }
    }

    private boolean isUpToDate(File source, File target) {
        if (target != null && target.exists()) {
            return FileUtils.getFileUtils().isUpToDate(target, source);
        }
        return true;
    }

    private boolean isInProject(Dependency dependency) {
        return loadFromProjectLib(dependency).exists();
    }

    private void download(Dependency dependency) {
        resolveRepository();

        for (String mirror : getRemoteSites(dependency)) {
            URL remoteArtifact = null;
            try {
                remoteArtifact = new URL(mirror + "/" + dependency);
            } catch (MalformedURLException mue) {
                throw new BuildException("Can not resolve url: " + remoteArtifact, mue);
            }

            File dest = getArtifactPath(localRepository, dependency);
            if (!dest.exists()) {
                mkdirTask.setDir(new File(dest.getParent()));
                mkdirTask.execute();
            }
            getTask.setDest(dest);
            getTask.setSrc(remoteArtifact);
            try {
                getTask.execute();
                return;
            } catch (BuildException e) {
                // Something wrong, we will check other site
                System.out.println("Trying simple form: " + dependency.simpleForm());
                try {
                    remoteArtifact = new URL(mirror + "/" + dependency.simpleForm());
                    getTask.setSrc(remoteArtifact);
                    getTask.execute();
                    return;
                } catch (Exception ea) {
                    // Failed again
                    if (verbose) {
                        System.err.println(ea.getMessage());
                    }
                }

                if (verbose) {
                    System.err.println(e.getMessage());
                }
            }
        }
        throw new BuildException("Can not download the artifact " + dependency + " from the remote sites.");
    }

    private File getArtifactPath(File repo, Dependency dependency) {
        return new File(repo, dependency.toString());
    }

    public void init() {
        super.init();

        getTask = new Get();

        getTask.setProject(getProject());
        getTask.setTaskName("fant.get");
        getTask.setVerbose(this.verbose);

        String privateBase = getProject().getProperty(PRIVATE_REPOSITORY_PROP_NAME);
        if (privateBase == null) {
            privateBase = getProject().getProperty("env." + PRIVATE_REPOSITORY_PROP_NAME);
        }
        if (privateBase != null && new File(privateBase).exists()) {
            privateRepository = new File(privateBase);
        }
    }

    private File loadFromProjectLib(Dependency dependency) {
        return new File(projectLib + "/" + dependency.getArtifact());
    }

    private File loadFromLocalBase(Dependency dependency) {
        File artifact = new File(localRepository, dependency.toString());
        if (artifact.exists()) {
            return artifact;
        }
        return null;
    }

    private File loadFromPrivateBase(Dependency dependency) {
        if (privateRepository != null) {
            File artifact = new File(privateRepository, dependency.toString());
            if (artifact.exists()) {
                return artifact;
            }
            return null;
        }
        if (verbose) {
            System.out.println("\n NO PRIVATE REPOSITORY defined in the project");
        }
        return null;
    }

    private Collection<String> getRemoteSites(final Dependency dependency) {
        Collection<String> rSites = new LinkedList<String>();
        Collection<String> xSites = new LinkedList<String>();
        
        for (String group : sites.keySet()) {
            if (dependency.getGroupid().startsWith(group)) {
                rSites.addAll(sites.get(group));
            } else {
                xSites.addAll(sites.get(group));
            }
        }
        rSites.addAll(xSites);
        return rSites;
    }

    private void resolveRepository() {
        if (!sites.isEmpty()) {
            return;
        }

        Repository repositories = (Repository) getProject().getReference(REMOTE_REPOSITORY_PROP_NAME);
        if (repositories == null) {
            return;
        }

        for (Enumeration e = repositories.getMirrors().elements(); e.hasMoreElements();) {
            Mirror mirror = (Mirror) e.nextElement();

            try {
                URL url = new URL(mirror.getUrl());
                String[] hostParts = url.getHost().split("\\.");
                StringBuffer group = new StringBuffer();
                group.append(hostParts[hostParts.length - 1]);
                group.append(".");
                group.append(hostParts[hostParts.length - 2]);
                
                storeSite(group.toString(), mirror.getUrl());
            } catch (MalformedURLException me) {
                me.printStackTrace();
            }

            if (mirror.getGroups() == null) {
                continue;
            }
            StringTokenizer groups = new StringTokenizer(mirror.getGroups(), ";");
            while (groups.hasMoreTokens()) {
                storeSite(groups.nextToken(), mirror.getUrl());
            }
        }
    }

    private void storeSite(String group, String site) {
        List<String> urls = sites.get(group);
        if (urls == null) {
            urls = new ArrayList<String>();
            sites.put(group, urls);
        }
        urls.add(site);
    }
    
    private void resolveProjectLib() {
        if (destdir != null) {
            projectLib = destdir;
            keepJars = true;
        } else {
            projectLib = new File(getProject().getBaseDir(), PROJECT_LIB_DIR);
        }
        mkdirTask.setDir(projectLib);
        mkdirTask.execute();
    }

    private void printInfo() {
        System.out.println();
        System.out.println("---------------------------------------------------");
        System.out.println("Project library: " + projectLib);
        System.out.println("Local repository: " + localRepository);
        System.out.println("Private repository: " + privateRepository);
        System.out.println("---------------------------------------------------");
        System.out.println();
    }

    public void setDepRef(Reference r) {
        Object o = null;
        try {
            o = r.getReferencedObject(getProject());
        } catch (Exception e) {
            log(e.getMessage(), Project.MSG_WARN);
            return;
        }

        if (!(o instanceof Dependencies)) {
            String msg = r.getRefId() + " doesn\'t denote a dependencies type";
            throw new BuildException(msg);
        }
        this.chained = ((Dependencies)o).isChained();
        this.dependencies = ((Dependencies)o).getDependencies();
    }
}
